显示英雄列表¶
首先我们的目的是在画面上显示一个简单的英雄列表,效果如下:

第一步 创建英雄类¶
在util库下创建文件hero-list.service.model.ts,存放英雄的定义:
1 2 3 4 5 6 7 | export interface Hero { heroId: string; name: string; gender: boolean; age: number; job: string; } |
然后在utils/src/index.ts中将其导出:
1 2 | export * from './lib/utils.module'; export * from './lib/hero-list.service.model'; |
第二步 在Reducer中修改状态¶
reducer里定义了画面中将会出现的状态,由于我们只想显示一个简单的英雄列表,所以状态中只有一个list变量
hero-list-store.reducer.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import { HeroListStoreAction, HeroListStoreActionTypes } from './hero-list-store.actions'; import { Hero } from '@sample-app/utils'; export const HEROLISTSTORE_FEATURE_KEY = 'heroListStore'; export interface HeroListStoreState { list: Hero[]; //想在画面上显示的数据的类型改为刚刚创建的Hero } export interface HeroListStorePartialState { readonly [HEROLISTSTORE_FEATURE_KEY]: HeroListStoreState; } //这是默认状态下的state定义 export const initialState: HeroListStoreState = { list: [] }; //state和action都是外部传入的 //state初始就是上面的默认定义 //action就是将要执行的动作类型 export function reducer( state: HeroListStoreState = initialState, action: HeroListStoreAction ): HeroListStoreState { switch (action.type) { //这个动作就是数据加载完成后的动作 case HeroListStoreActionTypes.HeroListStoreLoaded: { //将state中其他值不变,但是改变list的值为取得到的数据 state = { ...state, list: action.payload }; break; } } return state; } |
第三步 修改selector中的查询¶
selector.ts中会存放一些画面上将会执行到的查询
hero-list-store.selectors.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import { createFeatureSelector, createSelector } from '@ngrx/store'; import { HEROLISTSTORE_FEATURE_KEY, HeroListStoreState } from './hero-list-store.reducer'; // Lookup the 'HeroListStore' feature state managed by NgRx const getHeroListStoreState = createFeatureSelector<HeroListStoreState>( HEROLISTSTORE_FEATURE_KEY ); //这个查询将会直接返回所有英雄列表 const getAllHeroListStore = createSelector( getHeroListStoreState, (state: HeroListStoreState) => state.list ); export const heroListStoreQuery = { getAllHeroListStore }; |
第三步 修改facade中的变量¶
facade会存放一些查询执行后的结果,还有一些改变状态的方法等等
现在还并没有做过多的修改,只是将之前删掉的一些变量在这边也一同删去
hero-list-store.facade.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { HeroListStorePartialState } from './hero-list-store.reducer'; import { heroListStoreQuery } from './hero-list-store.selectors'; import { LoadHeroListStore } from './hero-list-store.actions'; @Injectable() export class HeroListStoreFacade { allHeroListStore$ = this.store.pipe( select(heroListStoreQuery.getAllHeroListStore) ); constructor(private store: Store<HeroListStorePartialState>) {} loadAll() { this.store.dispatch(new LoadHeroListStore()); } } |
第四步 修改action¶
action会定义在画面中所有可能执行到的动作
通常,如果在selector中新加了查询,那么这边一般也要添加一个动作
现在也没有做什么修改,只是将HeroListStoreLoaded中的类型改为了Hero
hero-list-store.actions.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import { Action } from '@ngrx/store'; import { Hero } from '@sample-app/utils'; export enum HeroListStoreActionTypes { LoadHeroListStore = '[HeroListStore] Load HeroListStore', HeroListStoreLoaded = '[HeroListStore] HeroListStore Loaded', HeroListStoreLoadError = '[HeroListStore] HeroListStore Load Error' } export class LoadHeroListStore implements Action { readonly type = HeroListStoreActionTypes.LoadHeroListStore; } export class HeroListStoreLoadError implements Action { readonly type = HeroListStoreActionTypes.HeroListStoreLoadError; constructor(public payload: any) {} } export class HeroListStoreLoaded implements Action { readonly type = HeroListStoreActionTypes.HeroListStoreLoaded; constructor(public payload: Hero[]) {} } export type HeroListStoreAction = | LoadHeroListStore | HeroListStoreLoaded | HeroListStoreLoadError; export const fromHeroListStoreActions = { LoadHeroListStore, HeroListStoreLoaded, HeroListStoreLoadError }; |
现在,store中需要进行的一些操作就完成了,接下来就要修改画面了
第五步 创建table结构¶
在libs/content/hero-lib/src/lib/hero-list下创建hero-list.header.ts类,存放表的定义
ngx-datatable会用到这些信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import { TableColumn } from '@swimlane/ngx-datatable'; export const formatter = { gender: { transform: val => (val ? '男' : '女') }, }; export const columns: TableColumn[] = [ { prop: 'heroId', name: '用户ID' }, { prop: 'name', name: '用户名' }, { prop: 'gender', name: '性别', pipe: formatter.gender }, { prop: 'age', name: '年龄' }, { prop: 'job', name: '职业' } ]; |
这里用到了@swimlane/ngx-datatable包,在npm中将其安装进来
1 | npm i @swimlane/ngx-datatable |
第六步 将要用到的控件包导入到module中¶
修改content-hero-lib.module.ts,将控件包导入进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; @NgModule({ imports: [ NgxDatatableModule, CommonModule, RouterModule.forChild([ {path: '', pathMatch: 'full', component: HeroListComponent} ]) ], declarations: [HeroListComponent] }) export class ContentHeroLibModule {} |
第六步 修改画面¶
hero-list.component.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <ngx-datatable #datatable [columns]="columns" [rows]="rows$ | async" class="material" [limit]="10" [headerHeight]="50" [footerHeight]="50" [rowHeight]="60" [scrollbarH]="true" [scrollbarV]="false" [selectionType]="false" > <!-- Template Column --> <ngx-datatable-column *ngFor="let col of columns" [width]="col.width" [name]="col.name" [prop]="col.prop" [pipe]="col.pipe" [cellClass]="col.cellClass" > </ngx-datatable-column> </ngx-datatable> |
hero-list.component.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { Hero } from '@sample-app/utils'; import { HeroListStoreFacade } from '@sample-app/store/hero-store'; import { columns } from './hero-list.header'; @Component({ selector: 'chs-hero-list-hero-list', templateUrl: './hero-list.component.html', styleUrls: ['./hero-list.component.css'] }) export class HeroListComponent implements OnInit { columns = columns; rows$: Observable<Hero[]>; constructor(public authUsers: HeroListStoreFacade) { this.rows$ = this.authUsers.allHeroListStore$; } ngOnInit() { } } |
第七步 修改错误¶
运行之后,发现画面为空,chrome中按F12打开控制台发现以下错误:

需要修改一些东西:
在apps/client/src/app/app.module.ts中,将一些包导入进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { NxModule } from '@nrwl/nx'; //导入的包 import { StoreModule } from '@ngrx/store'; //导入的包 import { EffectsModule } from '@ngrx/effects'; //导入的包 @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, AppRoutingModule, NxModule.forRoot(), //导入的包 StoreModule.forRoot({}), //导入的包 EffectsModule.forRoot([]) //导入的包 ], providers: [], bootstrap: [AppComponent] }) export class AppModule {} |
这个时候会发现@nrwl/nx不存在,在package.json中将其添加,然后执行npm install安装进项目中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ... "dependencies": { "@angular/animations": "^8.2.0", "@angular/common": "^8.2.0", "@angular/compiler": "^8.2.0", "@angular/core": "^8.2.0", "@angular/forms": "^8.2.0", "@angular/platform-browser": "^8.2.0", "@angular/platform-browser-dynamic": "^8.2.0", "@angular/router": "^8.2.0", "@ngrx/effects": "8.5.0", "@ngrx/entity": "8.5.0", "@ngrx/router-store": "8.5.0", "@ngrx/store": "8.5.0", "@nrwl/nx": "7.1.1", //添加@nrwl/nx "@swimlane/ngx-datatable": "^16.0.2", "core-js": "^2.5.4", "rxjs": "~6.4.0", "zone.js": "^0.9.1" }, ... |
然后是libs/content/hero-lib/src/lib/content-hero-lib.module.ts,需要将store的module导入进来
还要将facade进行依赖注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Router } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { StoreHeroStoreModule, HeroListStoreFacade } from '@sample-app/store/hero-store'; @NgModule({ imports: [ NgxDatatableModule, StoreHeroStoreModule, //导入store的module CommonModule, RouterModule.forChild([ {path: '', pathMatch: 'full', component: HeroListComponent} ]) ], declarations: [HeroListComponent] }) export class ContentHeroLibModule { //将Router和HeroListStoreFacade进行依赖注入,在这个module初始化的时候就将数据载入 constructor(private router: Router, private authUsers: HeroListStoreFacade) { this.router.events .subscribe(comp => { this.authUsers.loadAll(); //载入数据 }); } } |
最后是libs/store/hero-store/src/lib/+state/hero-list-store.effects.ts
这里要将DataPersistence类从@nrwl/angular改成从@nrwl/nx导入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import { Injectable } from '@angular/core'; import { Effect, Actions } from '@ngrx/effects'; import { DataPersistence } from '@nrwl/nx'; //修改导入的包 import { HeroListStorePartialState } from './hero-list-store.reducer'; import { LoadHeroListStore, HeroListStoreLoaded, HeroListStoreLoadError, HeroListStoreActionTypes } from './hero-list-store.actions'; @Injectable() export class HeroListStoreEffects { @Effect() loadHeroListStore$ = this.dataPersistence.fetch( HeroListStoreActionTypes.LoadHeroListStore, { run: (action: LoadHeroListStore, state: HeroListStorePartialState) => { // Your custom REST 'load' logic goes here. For now just return an empty list... return new HeroListStoreLoaded([]); }, onError: (action: LoadHeroListStore, error) => { console.error('Error', error); return new HeroListStoreLoadError(error); } } ); constructor( private actions$: Actions, private dataPersistence: DataPersistence<HeroListStorePartialState> ) {} } |
这些错误应该是因为版本问题导致的,参考的日本项目是7.*的版本,但是新创建的项目都是8.*
添加假数据¶
然后运行画面:

画面已经显示出来了,但是还没有数据
因为数据是从effect中加载的,在effect中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Effect() loadHeroListStore$ = this.dataPersistence.fetch( HeroListStoreActionTypes.LoadHeroListStore, { run: (action: LoadHeroListStore, state: HeroListStorePartialState) => { // Your custom REST 'load' logic goes here. For now just return an empty list... //这里返回了一个空的数据,所以画面上没有数据显示 return new HeroListStoreLoaded([]); }, onError: (action: LoadHeroListStore, error) => { console.error('Error', error); return new HeroListStoreLoadError(error); } } ); |
正式项目中,这里会从api取值,然后再从mock中返回假的值,这里简单处理就直接给常量
修改libs/src/lib/hero-list.service.model.ts,在这里设置一个固定的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | export interface Hero { heroId: string; name: string; gender: boolean; age: number; job: string; } export class HeroObject implements Hero { heroId: string; name: string; gender: boolean; age: number; job: string; } export let HERO_LIST: HeroObject[] = [ { heroId: 'hero1', name: 'JAMES', gender: true, age: 1 , job: `魔法师`}, { heroId: 'hero2', name: 'JOHN', gender: true, age: 12 , job: `战士`}, { heroId: 'hero3', name: 'ROBERT', gender: true, age: 13 , job: `圣骑士`}, { heroId: 'hero4', name: 'MICHAEL', gender: true, age: 14 , job: `牧师`}, { heroId: 'hero5', name: 'MARY', gender: false, age: 51 , job: `萨满`}, { heroId: 'hero6', name: 'PATRICIA', gender: false, age: 16 , job: `盗贼`}, { heroId: 'hero7', name: 'LINDA', gender: false, age: 17 , job: `德鲁伊`}, { heroId: 'hero8', name: 'BARBARA', gender: false, age: 81 , job: `术士`}, { heroId: 'hero9', name: 'ELIZABETH', gender: false, age: 19 , job: `猎人`}, { heroId: 'hero0', name: 'JENNIFER', gender: false, age: 20, job: `牧师` } ]; |
然后在effect中直接将常量返回:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import { Injectable } from '@angular/core'; import { Effect, Actions } from '@ngrx/effects'; import { DataPersistence } from '@nrwl/nx'; import { HeroListStorePartialState } from './hero-list-store.reducer'; import { LoadHeroListStore, HeroListStoreLoaded, HeroListStoreLoadError, HeroListStoreActionTypes } from './hero-list-store.actions'; import { HERO_LIST } from '@sample-app/utils'; import { map } from 'rxjs/operators'; import { of } from 'rxjs'; @Injectable() export class HeroListStoreEffects { @Effect() loadHeroListStore$ = this.dataPersistence.fetch( HeroListStoreActionTypes.LoadHeroListStore, { run: (action: LoadHeroListStore, state: HeroListStorePartialState) => { return of(HERO_LIST) .pipe(map(users => new HeroListStoreLoaded(users))); //直接返回常量 }, onError: (action: LoadHeroListStore, error) => { console.error('Error', error); return new HeroListStoreLoadError(error); } } ); constructor( private actions$: Actions, private dataPersistence: DataPersistence<HeroListStorePartialState> ) {} } |
运行项目:

现在画面上就有数据了